package com.saascloud.auth.core;

import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.access.AuthorizationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 自定义鉴权方法，依据访问url需要的权限 和令牌中已经有的权限对比，
 * 如果url需要的权限在令牌中有，则可以访问接口；
 * url需要的对应权限在数据库中预先配置
 *
 *接口权限暂时不验证，登陆或者接口放行的都可以访问接口
 *和原来通过拦截器直接放行一样，只要放行和有令牌的可以访问
 * @author liangxuch
 */
@Slf4j
public class MyAuthorityPermit {
    public static final String HAS_AUTHORITY = "hasAuthority";
    public static final String PERMIT_ALL = "permitAll";
    public static final String SUPER_API = "GET/superapi";
    public static final String OPTIONS = "OPTIONS";

    @Autowired
    OAuthConfig oauthConfig;

    @Autowired
    StringRedisTemplate redisTemplate;

    public boolean hasAuthority(HttpServletRequest request, Authentication authentication) {
        long start = System.currentTimeMillis();
        String method = request.getMethod();
        String servletPath = request.getServletPath();
        String apiPath = method + servletPath;
        // iam系统本身访问SUPER_API需要提前放行，否则会出现循环调用
        if (OPTIONS.equalsIgnoreCase(method)||apiPath.contains(SUPER_API)) {
            return true;
        }
        String userName = authentication.getName();
        // 如果访问接口是匿名，并且是放行
        if ("anonymousUser".equalsIgnoreCase(userName)) {
            log.info("hasAuthority.userName:{}-method:{}-apiPath:{}--{}", userName, method, apiPath, JSON.toJSONString(authentication));
            // 对外网关接口放行
            return  release(servletPath)|| release(apiPath) ;
        }

        String clientId = ((OAuth2Authentication) authentication).getOAuth2Request().getClientId();
        // 接口权限暂时不验证，已经登陆令牌有效或者接口放行的都可以访问接口
        // 和原来通过拦截器直接放行一样，只要放行和有令牌的可以访问
        //verifyAuthorization(apiPath, authentication, clientId);
        log.info("hasAuthority--{}ms--userName:{}-method:{}-apiPath:{}--clientId:{}--{}",System.currentTimeMillis()- start,userName, method, apiPath, clientId, JSON.toJSONString(authentication));
        return true;
    }


    private void verifyAuthorization(String apiPath, Authentication authentication, String clientId) {
        // 令牌中已有的权限
        Collection<? extends GrantedAuthority> alreadOwnedAuthorities = authentication.getAuthorities();
        // 客户端所有全权限信息
        JSONArray apiAuthorityMap = getApiAuthority(clientId);
        // 接口权限表没有配置，所有接口都需要在权限对应表中有对应关系，否则没有权限403
        if (apiAuthorityMap.isEmpty()) {
            // 已经认证但是没有权限403
            log.error("hasAuthority.verifyAuthorization--apiPath：{}--clientId:{}客户的的接口权限没有在数据库中配置", apiPath, clientId);
            throw new AuthorizationServiceException(apiPath + " is AccessDenied");
        }
        // 所有接口对应hasAuthority信息
        Map<String, String> hasAuthority = getApiAuthorityMap(apiAuthorityMap, HAS_AUTHORITY);
        // 请求地址在hasAuthority中查找key
        String authority = hasAuthority.get(apiPath);

        // 全路劲匹配在接口权限表中没有对应关系
        if (StringUtils.isEmpty(authority)) {
            authority = getAuthority(apiPath, clientId, apiAuthorityMap, hasAuthority);
        }
        // 如果在hasAuthority中查到对应的值，进一步确认接口需要的权限和用户需要的权限对比，如果没有403
        if (!StringUtils.isEmpty(authority)) {
            boolean hit = alreadOwnedAuthorities.contains(new SimpleGrantedAuthority(authority));
            if (!hit) {
                int n = apiPath.length() - apiPath.replace("/", "").length();
                for (int i = 0; i < n - 1; i++) {
                    hit = alreadOwnedAuthorities.contains(new SimpleGrantedAuthority(authority));
                    if (hit) {
                        break;
                    }
                }
                if (!hit && !release(apiPath)) {
                    // 已经认证但是没有权限403
                    log.info("hasAuthority.authentication---apiPath：{}--clientId:{}在接口权限表中有，但是令牌中没有相应权限", apiPath, clientId);
                    throw new AuthorizationServiceException(apiPath + " is AccessDenied");
                }
            }
        }

    }

    /**
     * 首先判断在hasAuthority中有
     * 如果没有再从permitAll权限中查
     * 如果还是没有，403
     *
     * @param apiPath         请求路径
     * @param clientId        客户的id
     * @param apiAuthorityMap 接口权限对应关系
     * @param hasAuthority    用户拥有权限
     */
    private String getAuthority(String apiPath, String clientId, JSONArray apiAuthorityMap, Map<String, String> hasAuthority) {
        String authority;
        // 截取路径循环查询
        authority = loopCheck(apiPath, hasAuthority);
        // 在HAS_AUTHORITY中没有
        if (StringUtils.isEmpty(authority)) {
            Map<String, String> permitAlls = getApiAuthorityMap(apiAuthorityMap, PERMIT_ALL);
            boolean hasPermitAll = permitAlls.containsKey(apiPath);
            if (!hasPermitAll) {
                authority = loopCheck(apiPath, permitAlls);
                if (StringUtils.isEmpty(authority) && !release(apiPath)) {
                    // 已经认证但是没有权限403
                    log.info("hasAuthority.verifyAuthorization--apiPath：{}--clientId:{}在接口权限表中都没有", apiPath, clientId);
                    throw new AuthorizationServiceException(apiPath + " is AccessDenied");
                }
            }
        }
        return authority;
    }

    /**
     * 接口路径中带有@PathVariable
     * 截取后循环匹配
     */
    private String loopCheck(String apiPath, Map<String, String> hasAuthority) {
        // 查询'/'几个,循环地址判断 PathVariable需要排除
        int n = apiPath.length() - apiPath.replace("/", "").length();
        for (int i = 0; i < n - 1; i++) {
            String path = apiPath.substring(0, apiPath.lastIndexOf('/'));
            String authority = hasAuthority.get(path);
            if (!StringUtils.isEmpty(authority)) {
                return authority;
            }
        }
        return null;
    }

    /**
     * 转换为key为apiPath，value为authority的map
     */
    private Map<String, String> getApiAuthorityMap(JSONArray apiAuthorityMap, String matchMode) {
        return apiAuthorityMap.stream().filter(data -> {
            JSONObject d = (JSONObject) data;
            return d.getString("matchMode").equalsIgnoreCase(matchMode);
        }).collect(Collectors.toMap(i -> {
            JSONObject a = (JSONObject) i;
            return a.getString("apiPath");
        }, i -> {
            JSONObject a = (JSONObject) i;
            String authority = a.getString("authority");
            if (StringUtils.isEmpty(authority)) {
                return "";
            }
            return authority;
        }));
    }

    /**
     * 根据客户端查询权限信息
     */
    private JSONArray getApiAuthority(String clientId) {
        String key = "oauth:authority:" + clientId;
        String apiPathAuthorityMap = redisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(apiPathAuthorityMap)) {
            HashMap<String, Object> map = new HashMap<>();
            map.put("clientId", clientId);
            JSONObject oauthClientDetails = getOauthClientDetails(clientId);
            String apiAuthority = oauthClientDetails.getJSONObject("data").getString("apiAuthority");
            String result = HttpUtil.get(apiAuthority, map);
            JSONObject jsonObject = JSON.parseObject(result);
            if (200 == jsonObject.getInteger("code")) {
                apiPathAuthorityMap = jsonObject.getString("data");
                redisTemplate.opsForValue().set(key, apiPathAuthorityMap, 12, TimeUnit.HOURS);
            }
        }
        if (StringUtils.isEmpty(apiPathAuthorityMap)) {
            log.info("hasAuthority.getApiAuthority--{}", clientId);
            return new JSONArray();
        }
        return JSON.parseArray(apiPathAuthorityMap);
    }

    /**
     * 接口是否被释放，是否被拦截
     * 接口是否拦截判断
     * 如果接口/xxx+/*在接口权限表中有，说明直接放行
     */
    protected boolean release(String apiPath) {
        int length = apiPath.length();
        int first = apiPath.indexOf('/');
        //从first + 1位置开始查找'/'
        int second = apiPath.indexOf('/', first + 1);
        // 如果没有第二个'/'，截取到末尾
        String subPath = apiPath.substring(0, (second == -1 ? length : second));
        JSONArray apiAuthorityMap = getApiAuthority(oauthConfig.getClientId());
        Map<String, String> permitAlls = getApiAuthorityMap(apiAuthorityMap, PERMIT_ALL);
        return permitAlls.containsKey(subPath + "/**");
    }


    /**
     * 检查接口是否可以放行
     * 如果放行就需要去掉网关的Bearer令牌
     * 如果不放行，保持不变
     * @param apiPath 请求路径
     * @return
     */
    protected boolean checkInterfaceRealeased(String apiPath) {
        JSONArray apiAuthorityMap = getApiAuthority(oauthConfig.getClientId());
        Map<String, String> permitAlls = getApiAuthorityMap(apiAuthorityMap, PERMIT_ALL);
        return permitAlls.containsKey(apiPath);
    }


    /**
     * 根据客户端id查客户端信息
     * @param clientId
     * @return
     */
    protected JSONObject getOauthClientDetails(String clientId) {
        String key = "oauth:clientdetail:" + clientId;
        String clientDetails = redisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(clientDetails)) {
            clientDetails = HttpUtil.get(oauthConfig.getOauthClientDetails() + "/" +clientId);
            redisTemplate.opsForValue().set(key,clientDetails,12,TimeUnit.HOURS);
        }
        JSONObject result = JSONObject.parseObject(clientDetails);
        log.info("getOauthClientDetails---{}--{}",key,result);
        return result;
    }


}
